[ 筆記 ] JavaScript - 02 函式


Posted by krebikshaw on 2020-06-22

函式的定義

JavaScript 函式定義有 3 種方式,分別在效率、解析順序、作用域有不同的特性。

// 1. 語句函式
function fun1(){  };

// 2. 函式賦值變數
var test = function(){  };

// 3. 建構式函數
var test2 = new Function();

編譯順序

JavaScript 在執行程式之前,會先對 語句函式 進行解析,再去一行一行執行程式,所以第一種 語句函式,在被呼叫之前,就會先解析好並儲存在內存中。

  • 以第一種宣告方式,由於函式會先行解析好,所以若是把呼叫函式的指令,寫在函式的程式碼上面,是可以成功呼叫到函式的。
  • 以第二種宣告方式 函式賦值變數,系統會先判讀有宣告變數 test 但還不會賦值。只有執行到該行程式碼時,才會把函式賦值給變數。所以若是把呼叫函式的指令,寫在函式的程式碼上面,系統會判斷為 undefined
fun1()
// 第一種宣告方式,可以先呼叫函式,再宣告。
function fun1() {  };

test()
// 第二種宣告方式,會判斷 test is not a function
var test = function() {  };

解析範例

看看以下經典範例來說明解析步驟:

  1. JavaScript解析器,會首先找到第一個test1( ) → 1
  2. 接下來找到第四個test1( ),並蓋掉第一個test1( ) → 4
  3. 再後來沒有function開頭語句了,所以才從頭一行一行執行並回傳
  4. 最後依序回傳 4, 2, 3, 3, 5, 6
function test1() { return 1; }
alert(test1());  // 4 → 被第四個 test1() 蓋掉

var test1 = function() { return 2; };
alert(test1());  // 2 

var test1 = new Function("return 3");
alert(test1()); // 3

function test1() { return 4; }
alert(test1()); // 3 → 第四個已經被解析過了所以跳過

var test1 = function() { return 5; };
alert(test1()); // 5

var test1 = new Function("return 6");
alert(test1()); // 6

執行效率

以效率方面來比較,相同的執行內容,所需的執行時間,大約是 1 = 2 < 3

  • 第一種及第二種方式,由於編譯完成後就會放在內存,使用時再呼叫,所以很適合多次呼叫的情形。
  • 第三種方式 建構式函式,使用後會被釋放掉,不會佔用內存,所以再一次呼叫時,要重新 new 一次,僅適合一次性呼叫。

作用域

  • 第一、二種方法具有函式作用域,return 時會得到宣告在函式內部的區域變數
  • 第三種 建構函式 方法具有頂級作用域,在函式中宣告的變數,會屬於全域變數。所以 return 時也會去抓全域變數的值。(這部分不是很理解,先暫時記住此特性)
var k = 1; //全域變數

function fun(){
  var k = 2; //區域變數

  // 函式作用域底下 (return 時會回傳 2)-----
  function test(){ return k; }
  var test = function() { return k; };

  // 頂級作用域底下 (return 時會回傳 1)-----
  var test = new Function("return k;");

  alert(test());
}

fun();

三種宣告方式比較

類型 語句函式 函式賦值變數 建構式函式
名稱 具名 匿名 匿名 |
性質 靜態 靜態 動態
解析時機 優先解析 順序解析 順序解析
多次呼叫效率
作用域 函式作用域 函式作用域 頂級作用域

調用函式

  • 這部分現在看不懂,先記下來,以後可能會用到
    執行函式,是在函式被調用時才開始,非函式定義時,Javascript有四種調用方式
  1. 作為函式(function)
  2. 作為方法(methods)
  3. 作為建構式(constructors)
  4. 透過 cell() 與 apply() 方法間接調用
    方法調用與函式調用有一個重要的差異:調用情境,即函式主體的 this 會視呼叫它的物件而定:
  • 當方法:其調用物件
  • 當函式:
    • strict 模式:全域物件
    • 非 strict 模式:undifined

return

函式中的 return 指令會使函式停止執行,並回傳運算結果(如果有的話),如果 return 的回傳值沒有東西,就會回傳 undifined。

  • 並非所有函式都有 return

選擇性參數

函式調用的 引數比參數少 時,額外參數會被設為 undifined,我們可以為 可能不存在 的參數設定 預設值

非必須的引數,需要寫在引數列的後面。

var obj1 = {a:"1", b:"2", c:"3"};
var obj2 = {d:"1", e:"2", f:"3"};

function getPropertyNames(o,a) { // o → 必須 ; a → 可以忽略
  a = a || []; // 如果a不存在就建一個新陣列
  for(var property in o) a.push(property); // 把o的特性值丟進a陣列
  return a;

}

var newObj = getPropertyNames(obj1); // 把obj1特性丟進新建的newObj
getPropertyNames(obj2, newObj); // 把obj2特性也丟進newObj

console.log(newObj); // ["a", "b", "c", "d", "e", "f"]

引數 Argument

JavaScript 提供了一個很方便的指令,可以讓我們查看傳入函式的 引數 是什麼。

  • Argument 是一種類陣列物件,可以用 length 來看長度,實際上 log 出來看,它其實是一個物件形式。
// 抓出最大值
Max(1,5,100,20); // 100

function Max() {
  var max = 0;
  var num = arguments; // 把引數陣列丟進num
  // num.length → 4
  // num[0] → 1 ; num[1] → 5; num[2] → 100...

  for (var i=0; i<num.length; i++) {
    if (max < num[i]) max = num[i]; // 當前元素大於max時,max更新
  }
  return max;
}

使用物件作為引數

當函式的參數比較多,可以利用物件 key: value 的特性,讓函式直接接收一個物件。

var a = [1,2,3,4], b = [];
copy({from:a, to:b, length:4});

function copy(obj) {
    // obj.from → [1, 2, 3, 4]
    // obj.form_start || 30 → 30
    // obj.to → []
    // obj.to_start: obj.to_start || 20 → 20
    // obj.length → 4
}

函式作為值

  • 函式也可以當做「值」,這代表他們可以被指定給變數、物件特性或陣列元素中,或是傳給函式當作引數用。
// 計算式
function add(x,y) { return x+y; }
function subtract(x,y) {return x-y; }

// 調用上面運算式做為第一個引數
function operate(method, num1, num2) {
  return method(num1, num2);
}

// a = 5+10
var a = operate(add, 5, 10);

// c = (10-5) + (2-1)
var c = operate(add, 
operate(subtract, 10, 5), 
operate(subtract, 2, 1));

函式的物件特性

函式是一種物件,代表也有特性可以存取,假設你要寫一個每次調用都回傳不同值的函式,且都要記錄上次存取的值,我們可以把資訊直接存在 函式特性

addCount.count = 0;
function addCount() {
  return addCount.count++;
}

addCount();
addCount();
addCount();

console.log( addCount.count ); // 3

call & apply方法

.call().apply() 可以用來傳遞參數用,但更重要的功能是用來間接調用某函式,當作是某物件的方法。

  • 第一個引數是指出要 在哪個物件上調用,在函式內容中成為this
    • call:任何多個引數 fun.call(object, num1, num2)
    • apply:一組陣列當引數 fun.apply(object, [num1, num2])
//傳遞參數用--------------
function sum(x,y) {
  return x+y;
}

// call 
function callTest(num1, num2) {
  return sum.call(this, num1, num2);
}
// apply 
function applyTest(num1, num2) {
  return sum.apply(this, [num1, num2]);
}
/* apply 也可以改寫用 arguments
function applyTest() {
  return sum.apply(this, arguments);
}
*/

callTest(10,20);  //30
applyTest(10,50); //60

間接調用函式

//間接調用函式,當做是物件的方法--------------
function sum(x,y) {
  return x+y;
}

function Obj(x,y) {
  this.x = x;
  this.y = y;
}

var o = new Obj();

//方法.apply(作用域, [參數陣列])
sum.apply(o, [10,100]); // 110

Closures 閉包

函式執行完畢後,變數會被釋放掉,但是我們可以利用在函式中宣告另一個函式,並把它 return 出去,來 keep 住我們要保留的變數資料

EC: Execution Context,每個進入一個 工作 BLOCK,會產生一個新的 EC,每次進入一個新的 EC,Scope chain 就會被建立。
VO: Variable Object,在建立 Global EC 的時候,用來紀錄初始化變數的地方。

Global EC: {
    VO: {
        str: undefined
        fun1: function
    }
}

AO: Activation Object,用來在新增 function EC 的時候,紀錄初始化變數的地方。跟 Variable Object 很像,差別只在 AO 是在新稱 function EC 的時候產生。

function EC: {
    AO: {
        arr: undefined
        fun2: function
    }
    scopeChain: [function EC.AO, [Scope]]
}

Block 運作方式:

// 請先記住這段程式碼!! 下面會針對這段程式碼來做說明
var a = 1
function test() {
    var b = 2
    function inner() {
        var c = 3
        console.log(b)
        console.log(a)
    }
    return inner()
}
test()

模擬 JS 引擎:

  1. 程式開始執行,產生 Global EC,並初始化 VO,建立自己的 ScopeChain
    Global EC: {
        VO: {
            a: undefined
            test: function
        }
        ScopeChain: [global EC.VO]
    }
    // test.[[scope]] 初始化
    test.[[Scope]] = global EC.ScopeChain  // global EC.VO
    
  2. 開始跑程式,跑到 var a=1 的時候,將 global EC.VO.a 改成 1
  3. 進入 test function 之後,產生一個 test EC 並初始化 AO
    test EC: {
       AO: {
           b: undefined
           inner: function
       }
       ScopeChain: [test EC.AO, test.[[scope]]]
       // test EC.AO, global EC.ScopeChain
    }
    // inner.[[scope]] 初始化
    inner.[[scope]] = test EC.ScopeChain // test EC.AO, global EC.ScopeChain
    
  4. 執行到 var b=2 的時候,將 test EC.AO.b 改成 2
  5. 進入 inner 之後,產生一個 inner EC,初始化 AO
    inner EC: {
       AO: {
           c: undefined
       }
       ScopeChain: [inner EC.AO, inner.[[scope]]]
       // inner EC.AO, test EC.ScopeChain
       // inner EC.AO, test EC.AO, global EC.VO
    }
    
  6. 執行到 var c=3 的時候,將 inner EC.AO.c 改成 3
  7. 執行 console.log(b) 會先在 inner EC.AO 找,找不到再去 test EC.AO 找,找到了 b,就 log 出 b = 2
  8. 執行 console.log(a) 會先在 inner EC.AO 找,找不到再去 test EC.AO 找,找不到再去 global EC.VO 找,找到了,就 log 出 a = 1

ScopeChain 說穿了就是從自己所在的 Executin Context,開始一路把到 global EC 過程中的 AO 及 VO 串通通起來。

因為內部的函式,它的 ScopeChain 含有外層函式的 AO,所以內部函式被 return 出去,系統不能把外層函式的 AO 釋放掉,因而保留了變數的資料。

Closure 就是函式結束後要將其釋放,卻因為有保留 ScopeChain,所以即便已經結束函式了,還是可以存取到函式裡的值。

參考資料:JavaScript大全(第六版)


#javascript #函式 #function







Related Posts

如何在終端機下使用 conda 指令管理虛擬環境

如何在終端機下使用 conda 指令管理虛擬環境

如何優雅的利用marktext撰寫markdown

如何優雅的利用marktext撰寫markdown

Secure Apache Using Certbot with Let's Encrypt on Ubuntu 20.04

Secure Apache Using Certbot with Let's Encrypt on Ubuntu 20.04


Comments